1//////////////////////////////////////////////////////////////////////
2// LibFile: drawing.scad
3// This file includes stroke(), which converts a path into a
4// geometric object, like drawing with a pen. It even works on
5// three-dimensional paths. You can make a dashed line or add arrow
6// heads. The turtle() function provides a turtle graphics style
7// approach for producing paths. The arc() function produces arc paths,
8// and helix() produces helical paths.
9// Includes:
10// include <BOSL2/std.scad>
11// FileGroup: Basic Modeling
12// FileSummary: Create and draw 2D and 3D paths: arc, helix, turtle graphics
13// FileFootnotes: STD=Included in std.scad
14//////////////////////////////////////////////////////////////////////
15
16
17// Section: Line Drawing
18
19// Module: stroke()
20// Synopsis: Draws a line along a path or region boundry.
21// SynTags: Geom
22// Topics: Paths (2D), Paths (3D), Drawing Tools
23// See Also: dashed_stroke(), offset_stroke(), path_sweep()
24// Usage:
25// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]);
26// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]);
27// Description:
28// Draws a 2D or 3D path with a given line width. Joints and each endcap can be replaced with
29// various marker shapes, and can be assigned different colors. If passed a region instead of
30// a path, draws each path in the region as a closed polygon by default. If `closed=false` is
31// given with a region or list of paths, then each path is drawn without the closing line segment.
32// When drawing a closed path or region, there are no endcaps, so you cannot give the endcap parameters.
33// To facilitate debugging, stroke() accepts "paths" that have a single point. These are drawn with
34// the style of endcap1, but have their own scale parameter, `singleton_scale`, which defaults to 2
35// so that singleton dots with endcap "round" are clearly visible.
36// .
37// In 2d the stroke module works by creating a sequence of rectangles (or trapezoids if line width varies) and
38// filling in the gaps with rounded wedges. This is fast and produces a good result. In 3d the modules
39// creates a cylinders (or cones) and fills the gaps with rounded wedges made using rotate_extrude. This process will be slow for
40// long paths due to the 3d unions, and the faces on sequential cylinders may not line up. In many cases, {{path_sweep()}} will be
41// a better choice, both running faster and producing superior output, when working in three dimensions.
42// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types
43// cap_pairs = [
44// ["butt", "chisel" ],
45// ["round", "square" ],
46// ["line", "cross" ],
47// ["x", "diamond"],
48// ["dot", "block" ],
49// ["tail", "arrow" ],
50// ["tail2", "arrow2" ]
51// ];
52// for (i = idx(cap_pairs)) {
53// fwd((i-len(cap_pairs)/2+0.5)*13) {
54// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]);
55// color("black") {
56// stroke([[-20,0], [20,0]], width=0.25, endcaps=false);
57// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center");
58// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center");
59// }
60// }
61// }
62// Arguments:
63// path = The path to draw along.
64// width = The width of the line to draw. If given as a list of widths, (one for each path point), draws the line with varying thickness to each point.
65// closed = If true, draw an additional line from the end of the path to the start.
66// joints = Specifies the joint shape for each joint of the line. If a 2D polygon is given, use that to draw custom joints.
67// endcaps = Specifies the endcap type for both ends of the line. If a 2D polygon is given, use that to draw custom endcaps.
68// endcap1 = Specifies the endcap type for the start of the line. If a 2D polygon is given, use that to draw a custom endcap.
69// endcap2 = Specifies the endcap type for the end of the line. If a 2D polygon is given, use that to draw a custom endcap.
70// dots = Specifies both the endcap and joint types with one argument. If given `true`, sets both to "dot". If a 2D polygon is given, uses that to draw custom dots.
71// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width.
72// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width.
73// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width.
74// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width.
75// dots_width = This specifies the size of the joints and endcaps, in multiples of the line width.
76// joint_length = Length of joint shape, in multiples of the line width.
77// endcap_length = Length of endcaps, in multiples of the line width.
78// endcap_length1 = Length of starting endcap, in multiples of the line width.
79// endcap_length2 = Length of ending endcap, in multiples of the line width.
80// dots_length = Length of both joints and endcaps, in multiples of the line width.
81// joint_extent = Extents length of joint shape, in multiples of the line width.
82// endcap_extent = Extents length of endcaps, in multiples of the line width.
83// endcap_extent1 = Extents length of starting endcap, in multiples of the line width.
84// endcap_extent2 = Extents length of ending endcap, in multiples of the line width.
85// dots_extent = Extents length of both joints and endcaps, in multiples of the line width.
86// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun (for 3D lines).
87// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun (for 3D lines).
88// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
89// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
90// dots_angle = Extra rotation given to both joints and endcaps, in degrees. If not given, the endcap is fully spun (for 3D lines).
91// trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps.
92// trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap.
93// trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap.
94// color = If given, sets the color of the line segments, joints and endcap.
95// endcap_color = If given, sets the color of both endcaps. Overrides `color=` and `dots_color=`.
96// endcap_color1 = If give, sets the color of the starting endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
97// endcap_color2 = If given, sets the color of the ending endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
98// joint_color = If given, sets the color of the joints. Overrides `color=` and `dots_color=`.
99// dots_color = If given, sets the color of the endcaps and joints. Overrides `color=`.
100// singleton_scale = Change the scale of the endcap shape drawn for singleton paths. Default: 2.
101// convexity = Max number of times a line could intersect a wall of an endcap.
102// Example(2D): Drawing a Path
103// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
104// stroke(path, width=20);
105// Example(2D): Closing a Path
106// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
107// stroke(path, width=20, closed=true);
108// Example(2D): Fancy Arrow Endcaps
109// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
110// stroke(path, width=10, endcaps="arrow2");
111// Example(2D): Modified Fancy Arrow Endcaps
112// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
113// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2);
114// Example(2D): Mixed Endcaps
115// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
116// stroke(path, width=10, endcap1="tail2", endcap2="arrow2");
117// Example(2D): Plotting Points. Setting endcap_angle to zero results in the weird arrow orientation.
118// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
119// stroke(path, width=3, joints="diamond", endcaps="arrow2", endcap_angle=0, endcap_width=5, joint_angle=0, joint_width=5);
120// Example(2D): Default joint gives curves along outside corners of the path:
121// stroke([square(40)], width=18);
122// Example(2D): Setting `joints="square"` gives flat outside corners
123// stroke([square(40)], width=18, joints="square");
124// Example(2D): Setting `joints="butt"` does not draw any transitions, just rectangular strokes for each segment, meeting at their centers:
125// stroke([square(40)], width=18, joints="butt");
126// Example(2D): Joints and Endcaps
127// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
128// stroke(path, width=8, joints="dot", endcaps="arrow2");
129// Example(2D): Custom Endcap Shapes
130// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
131// arrow = [[0,0], [2,-3], [0.5,-2.3], [2,-4], [0.5,-3.5], [-0.5,-3.5], [-2,-4], [-0.5,-2.3], [-2,-3]];
132// stroke(path, width=10, trim=3.5, endcaps=arrow);
133// Example(2D): Variable Line Width
134// path = circle(d=50,$fn=18);
135// widths = [for (i=idx(path)) 10*i/len(path)+2];
136// stroke(path,width=widths,$fa=1,$fs=1);
137// Example: 3D Path with Endcaps
138// path = rot([15,30,0], p=path3d(pentagon(d=50)));
139// stroke(path, width=2, endcaps="arrow2", $fn=18);
140// Example: 3D Path with Flat Endcaps
141// path = rot([15,30,0], p=path3d(pentagon(d=50)));
142// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18);
143// Example: 3D Path with Mixed Endcaps
144// path = rot([15,30,0], p=path3d(pentagon(d=50)));
145// stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18);
146// Example: 3D Path with Joints and Endcaps
147// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]];
148// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18);
149// Example: Coloring Lines, Joints, and Endcaps
150// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i),20*sin(2*i)]];
151// stroke(
152// path, width=2, joints="dot", endcap1="dot", endcap2="arrow2",
153// color="lightgreen", joint_color="red", endcap_color="blue",
154// joint_width=2.0, endcap_width2=3, $fn=18
155// );
156// Example(2D): Simplified Plotting
157// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i)]];
158// stroke(path, width=2, dots=true, color="lightgreen", dots_color="red", $fn=18);
159// Example(2D): Drawing a Region
160// rgn = [square(100,center=true), circle(d=60,$fn=18)];
161// stroke(rgn, width=2);
162// Example(2D): Drawing a List of Lines
163// paths = [
164// for (y=[-60:60:60]) [
165// for (a=[-180:15:180])
166// [a, 2*y+60*sin(a+y)]
167// ]
168// ];
169// stroke(paths, closed=false, width=5);
170// Example(2D): Paths with a singleton. Note that the singleton is not a single point, but a list containing a single point.
171// stroke([
172// [[0,0],[1,1]],
173// [[1.5,1.5]],
174// [[2,2],[3,3]]
175// ],width=0.2,closed=false,$fn=16);
176function stroke(
177 path, width=1, closed,
178 endcaps, endcap1, endcap2, joints, dots,
179 endcap_width, endcap_width1, endcap_width2, joint_width, dots_width,
180 endcap_length, endcap_length1, endcap_length2, joint_length, dots_length,
181 endcap_extent, endcap_extent1, endcap_extent2, joint_extent, dots_extent,
182 endcap_angle, endcap_angle1, endcap_angle2, joint_angle, dots_angle,
183 endcap_color, endcap_color1, endcap_color2, joint_color, dots_color, color,
184 trim, trim1, trim2, singleton_scale=2,
185 convexity=10
186) = no_function("stroke");
187
188
189module stroke(
190 path, width=1, closed,
191 endcaps, endcap1, endcap2, joints, dots,
192 endcap_width, endcap_width1, endcap_width2, joint_width, dots_width,
193 endcap_length, endcap_length1, endcap_length2, joint_length, dots_length,
194 endcap_extent, endcap_extent1, endcap_extent2, joint_extent, dots_extent,
195 endcap_angle, endcap_angle1, endcap_angle2, joint_angle, dots_angle,
196 endcap_color, endcap_color1, endcap_color2, joint_color, dots_color, color,
197 trim, trim1, trim2, singleton_scale=2,
198 convexity=10
199) {
200 no_children($children);
201 module setcolor(clr) {
202 if (clr==undef) {
203 children();
204 } else {
205 color(clr) children();
206 }
207 }
208 function _shape_defaults(cap) =
209 cap==undef? [1.00, 0.00, 0.00] :
210 cap==false? [1.00, 0.00, 0.00] :
211 cap==true? [1.00, 1.00, 0.00] :
212 cap=="butt"? [1.00, 0.00, 0.00] :
213 cap=="round"? [1.00, 1.00, 0.00] :
214 cap=="chisel"? [1.00, 1.00, 0.00] :
215 cap=="square"? [1.00, 1.00, 0.00] :
216 cap=="block"? [2.00, 1.00, 0.00] :
217 cap=="diamond"? [2.50, 1.00, 0.00] :
218 cap=="dot"? [2.00, 1.00, 0.00] :
219 cap=="x"? [2.50, 0.40, 0.00] :
220 cap=="cross"? [3.00, 0.33, 0.00] :
221 cap=="line"? [3.50, 0.22, 0.00] :
222 cap=="arrow"? [3.50, 0.40, 0.50] :
223 cap=="arrow2"? [3.50, 1.00, 0.14] :
224 cap=="tail"? [3.50, 0.47, 0.50] :
225 cap=="tail2"? [3.50, 0.28, 0.50] :
226 is_path(cap)? [0.00, 0.00, 0.00] :
227 assert(false, str("Invalid cap or joint: ",cap));
228
229 function _shape_path(cap,linewidth,w,l,l2) = (
230 cap=="butt" || cap==false || cap==undef ? [] :
231 cap=="round" || cap==true ? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) :
232 cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) :
233 cap=="diamond"? circle(d=w,$fn=4) :
234 cap=="square"? scale([w,l], p=square(1,center=true)) :
235 cap=="block"? scale([w,l], p=square(1,center=true)) :
236 cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) :
237 cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+l/2,w-l/2]/2, [w-l/2,w+l/2]/2, [0,l/2]]) ] :
238 cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] :
239 cap=="line"? scale([w,l], p=square(1,center=true)) :
240 cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] :
241 cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] :
242 cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] :
243 cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] :
244 is_path(cap)? cap :
245 assert(false, str("Invalid endcap: ",cap))
246 ) * linewidth;
247
248 closed = default(closed, is_region(path));
249 check1 = assert(is_bool(closed))
250 assert(!closed || num_defined([endcaps,endcap1,endcap2])==0, "Cannot give endcap parameter(s) with closed path or region");
251
252 dots = dots==true? "dot" : dots;
253
254 endcap1 = first_defined([endcap1, endcaps, dots, "round"]);
255 endcap2 = first_defined([endcap2, endcaps, if (!closed) dots, "round"]);
256 joints = first_defined([joints, dots, "round"]);
257 check2 =
258 assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1))
259 assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2))
260 assert(is_bool(joints) || is_string(joints) || is_path(joints));
261
262 endcap1_dflts = _shape_defaults(endcap1);
263 endcap2_dflts = _shape_defaults(endcap2);
264 joint_dflts = _shape_defaults(joints);
265
266 endcap_width1 = first_defined([endcap_width1, endcap_width, dots_width, endcap1_dflts[0]]);
267 endcap_width2 = first_defined([endcap_width2, endcap_width, dots_width, endcap2_dflts[0]]);
268 joint_width = first_defined([joint_width, dots_width, joint_dflts[0]]);
269
270 endcap_length1 = first_defined([endcap_length1, endcap_length, dots_length, endcap1_dflts[1]*endcap_width1]);
271 endcap_length2 = first_defined([endcap_length2, endcap_length, dots_length, endcap2_dflts[1]*endcap_width2]);
272 joint_length = first_defined([joint_length, dots_length, joint_dflts[1]*joint_width]);
273
274 endcap_extent1 = first_defined([endcap_extent1, endcap_extent, dots_extent, endcap1_dflts[2]*endcap_width1]);
275 endcap_extent2 = first_defined([endcap_extent2, endcap_extent, dots_extent, endcap2_dflts[2]*endcap_width2]);
276 joint_extent = first_defined([joint_extent, dots_extent, joint_dflts[2]*joint_width]);
277
278 endcap_angle1 = first_defined([endcap_angle1, endcap_angle, dots_angle]);
279 endcap_angle2 = first_defined([endcap_angle2, endcap_angle, dots_angle]);
280 joint_angle = first_defined([joint_angle, dots_angle]);
281
282 check3 =
283 assert(all_nonnegative([endcap_length1]))
284 assert(all_nonnegative([endcap_length2]))
285 assert(all_nonnegative([joint_length]));
286 assert(all_nonnegative([endcap_extent1]))
287 assert(all_nonnegative([endcap_extent2]))
288 assert(all_nonnegative([joint_extent]));
289 assert(is_undef(endcap_angle1)||is_finite(endcap_angle1))
290 assert(is_undef(endcap_angle2)||is_finite(endcap_angle2))
291 assert(is_undef(joint_angle)||is_finite(joint_angle))
292 assert(all_positive([singleton_scale]))
293 assert(all_positive(width));
294
295 endcap_color1 = first_defined([endcap_color1, endcap_color, dots_color, color]);
296 endcap_color2 = first_defined([endcap_color2, endcap_color, dots_color, color]);
297 joint_color = first_defined([joint_color, dots_color, color]);
298
299 // We want to allow "paths" with length 1, so we can't use the normal path/region checks
300 paths = is_matrix(path) ? [path] : path;
301 assert(is_list(paths),"The path argument must be a list of 2D or 3D points, or a region.");
302 attachable(){
303 for (path = paths) {
304 pathvalid = is_path(path,[2,3]) || same_shape(path,[[0,0]]) || same_shape(path,[[0,0,0]]);
305 assert(pathvalid,"The path argument must be a list of 2D or 3D points, or a region.");
306
307 check4 = assert(is_num(width) || len(width)==len(path),
308 "width must be a number or a vector the same length as the path (or all components of a region)");
309 path = deduplicate( closed? list_wrap(path) : path );
310 width = is_num(width)? [for (x=path) width]
311 : closed? list_wrap(width)
312 : width;
313 check4a=assert(len(width)==len(path), "path had duplicated points and width was given as a list: this is not allowd");
314
315 endcap_shape1 = _shape_path(endcap1, width[0], endcap_width1, endcap_length1, endcap_extent1);
316 endcap_shape2 = _shape_path(endcap2, last(width), endcap_width2, endcap_length2, endcap_extent2);
317
318 trim1 = width[0] * first_defined([
319 trim1, trim,
320 (endcap1=="arrow")? endcap_length1-0.01 :
321 (endcap1=="arrow2")? endcap_length1*3/4 :
322 0
323 ]);
324
325 trim2 = last(width) * first_defined([
326 trim2, trim,
327 (endcap2=="arrow")? endcap_length2-0.01 :
328 (endcap2=="arrow2")? endcap_length2*3/4 :
329 0
330 ]);
331 check10 = assert(is_finite(trim1))
332 assert(is_finite(trim2));
333
334 if (len(path) == 1) {
335 if (len(path[0]) == 2) {
336 // Endcap1
337 setcolor(endcap_color1) {
338 translate(path[0]) {
339 mat = is_undef(endcap_angle1)? ident(3) : zrot(endcap_angle1);
340 multmatrix(mat) polygon(scale(singleton_scale,endcap_shape1));
341 }
342 }
343 } else {
344 // Endcap1
345 setcolor(endcap_color1) {
346 translate(path[0]) {
347 $fn = segs(width[0]/2);
348 if (is_undef(endcap_angle1)) {
349 rotate_extrude(convexity=convexity) {
350 right_half(planar=true) {
351 polygon(endcap_shape1);
352 }
353 }
354 } else {
355 rotate([90,0,endcap_angle1]) {
356 linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) {
357 polygon(endcap_shape1);
358 }
359 }
360 }
361 }
362 }
363 }
364 } else {
365 dummy=assert(trim1<path_length(path)-trim2, "Path is too short for endcap(s). Try a smaller width, or set endcap_length to a smaller value.");
366 // This section shortens the path to allow room for the specified endcaps. Note that if
367 // the path is closed, there are not endcaps, so we don't shorten the path, but in that case we
368 // duplicate entry 1 so that the path wraps around a little more and we can correctly create all the joints.
369 // (Why entry 1? Because entry 0 was already duplicated by a list_wrap() call.)
370 pathcut = path_cut_points(path, [trim1, path_length(path)-trim2], closed=false);
371 pathcut_su = _cut_to_seg_u_form(pathcut,path);
372 path2 = closed ? [each path, path[1]]
373 : _path_cut_getpaths(path, pathcut, closed=false)[1];
374 widths = closed ? [each width, width[1]]
375 : _path_select(width, pathcut_su[0][0], pathcut_su[0][1], pathcut_su[1][0], pathcut_su[1][1]);
376 start_vec = path[0] - path[1];
377 end_vec = last(path) - select(path,-2);
378
379 if (len(path[0]) == 2) { // Two dimensional case
380 // Straight segments
381 setcolor(color) {
382 for (i = idx(path2,e=-2)) {
383 seg = select(path2,i,i+1);
384 delt = seg[1] - seg[0];
385 translate(seg[0]) {
386 rot(from=BACK,to=delt) {
387 trapezoid(w1=widths[i], w2=widths[i+1], h=norm(delt), anchor=FRONT);
388 }
389 }
390 }
391 }
392
393 // Joints
394 setcolor(joint_color) {
395 for (i = [1:1:len(path2)-2]) {
396 $fn = quantup(segs(widths[i]/2),4);
397 translate(path2[i]) {
398 if (joints != undef && joints != "round" && joints != "square") {
399 joint_shape = _shape_path(
400 joints, widths[i],
401 joint_width,
402 joint_length,
403 joint_extent
404 );
405 v1 = unit(path2[i] - path2[i-1]);
406 v2 = unit(path2[i+1] - path2[i]);
407 mat = is_undef(joint_angle)
408 ? rot(from=BACK,to=v1)
409 : zrot(joint_angle);
410 multmatrix(mat) polygon(joint_shape);
411 } else {
412 // These are parallel to the path
413 v1 = path2[i] - path2[i-1];
414 v2 = path2[i+1] - path2[i];
415 ang = modang(v_theta(v2) - v_theta(v1));
416 // Need 90 deg offset to make wedge perpendicular to path, and the wedge
417 // position depends on whether we turn left (ang<0) or right (ang>0)
418 theta = v_theta(v1) - sign(ang)*90;
419
420 if (!approx(ang,0)){
421 // This section creates a rounded wedge to fill in gaps. The wedge needs to be oversized for overlap
422 // in all directions, including its apex, but not big enough to create artifacts.
423 // The core of the wedge is the proper arc we need to create. We then add side points based
424 // on firstang and secondang, where we try 1 degree, but if that appears too big we based it
425 // on the segment length. We pick the radius based on the smaller of the width at this point
426 // and the adjacent width, which could be much smaller---meaning that we need a much smaller radius.
427 // The apex offset we pick to be simply based on the width at this point.
428 firstang = sign(ang)*min(1,0.5*norm(v1)/PI/widths[i]*360);
429 secondang = sign(ang)*min(1,0.5*norm(v2)/PI/widths[i]*360);
430 firstR = 0.5*min(widths[i], lerp(widths[i],widths[i-1], abs(firstang)*PI*widths[i]/360/norm(v1)));
431 secondR = 0.5*min(widths[i], lerp(widths[i],widths[i+1], abs(secondang)*PI*widths[i]/360/norm(v2)));
432 apex_offset = widths[i]/10;
433 arcpath = [
434 firstR*[cos(theta-firstang), sin(theta-firstang)],
435 each arc(d=widths[i], angle=[theta, theta+ang],n=joints=="square"?2:undef),
436 secondR*[cos(theta+ang+secondang), sin(theta+ang+secondang)],
437 -apex_offset*[cos(theta+ang/2), sin(theta+ang/2)]
438 ];
439 polygon(arcpath);
440 }
441 }
442 }
443 }
444 }
445 if (!closed){
446 // Endcap1
447 setcolor(endcap_color1) {
448 translate(path[0]) {
449 mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) :
450 zrot(endcap_angle1);
451 multmatrix(mat) polygon(endcap_shape1);
452 }
453 }
454
455 // Endcap2
456 setcolor(endcap_color2) {
457 translate(last(path)) {
458 mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) :
459 zrot(endcap_angle2);
460 multmatrix(mat) polygon(endcap_shape2);
461 }
462 }
463 }
464 } else { // Three dimensional case
465 rotmats = cumprod([
466 for (i = idx(path2,e=-2)) let(
467 vec1 = i==0? UP : unit(path2[i]-path2[i-1], UP),
468 vec2 = unit(path2[i+1]-path2[i], UP)
469 ) rot(from=vec1,to=vec2)
470 ]);
471
472 sides = [
473 for (i = idx(path2,e=-2))
474 quantup(segs(max(widths[i],widths[i+1])/2),4)
475 ];
476
477 // Straight segments
478 setcolor(color) {
479 for (i = idx(path2,e=-2)) {
480 dist = norm(path2[i+1] - path2[i]);
481 w1 = widths[i]/2;
482 w2 = widths[i+1]/2;
483 $fn = sides[i];
484 translate(path2[i]) {
485 multmatrix(rotmats[i]) {
486 cylinder(r1=w1, r2=w2, h=dist, center=false);
487 }
488 }
489 }
490 }
491
492 // Joints
493 setcolor(joint_color) {
494 for (i = [1:1:len(path2)-2]) {
495 $fn = sides[i];
496 translate(path2[i]) {
497 if (joints != undef && joints != "round") {
498 joint_shape = _shape_path(
499 joints, width[i],
500 joint_width,
501 joint_length,
502 joint_extent
503 );
504 multmatrix(rotmats[i] * xrot(180)) {
505 $fn = sides[i];
506 if (is_undef(joint_angle)) {
507 rotate_extrude(convexity=convexity) {
508 right_half(planar=true) {
509 polygon(joint_shape);
510 }
511 }
512 } else {
513 rotate([90,0,joint_angle]) {
514 linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) {
515 polygon(joint_shape);
516 }
517 }
518 }
519 }
520 } else {
521 corner = select(path2,i-1,i+1);
522 axis = vector_axis(corner);
523 ang = vector_angle(corner);
524 if (!approx(ang,0)) {
525 frame_map(x=path2[i-1]-path2[i], z=-axis) {
526 zrot(90-0.5) {
527 rotate_extrude(angle=180-ang+1) {
528 arc(d=widths[i], start=-90, angle=180);
529 }
530 }
531 }
532 }
533 }
534 }
535 }
536 }
537 if (!closed){
538 // Endcap1
539 setcolor(endcap_color1) {
540 translate(path[0]) {
541 multmatrix(rotmats[0] * xrot(180)) {
542 $fn = sides[0];
543 if (is_undef(endcap_angle1)) {
544 rotate_extrude(convexity=convexity) {
545 right_half(planar=true) {
546 polygon(endcap_shape1);
547 }
548 }
549 } else {
550 rotate([90,0,endcap_angle1]) {
551 linear_extrude(height=max(widths[0],0.001), center=true, convexity=convexity) {
552 polygon(endcap_shape1);
553 }
554 }
555 }
556 }
557 }
558 }
559
560 // Endcap2
561 setcolor(endcap_color2) {
562 translate(last(path)) {
563 multmatrix(last(rotmats)) {
564 $fn = last(sides);
565 if (is_undef(endcap_angle2)) {
566 rotate_extrude(convexity=convexity) {
567 right_half(planar=true) {
568 polygon(endcap_shape2);
569 }
570 }
571 } else {
572 rotate([90,0,endcap_angle2]) {
573 linear_extrude(height=max(last(widths),0.001), center=true, convexity=convexity) {
574 polygon(endcap_shape2);
575 }
576 }
577 }
578 }
579 }
580 }
581 }
582 }
583 }
584 }
585 union();
586 }
587}
588
589
590// Function&Module: dashed_stroke()
591// Synopsis: Draws a dashed line along a path or region boundry.
592// SynTags: Geom, PathList
593// Topics: Paths, Drawing Tools
594// See Also: stroke(), path_cut()
595// Usage: As a Module
596// dashed_stroke(path, dashpat, [width=], [closed=]);
597// Usage: As a Function
598// dashes = dashed_stroke(path, dashpat, [closed=]);
599// Description:
600// Given a path (or region) and a dash pattern, creates a dashed line that follows that
601// path or region boundary with the given dash pattern.
602// - When called as a function, returns a list of dash sub-paths.
603// - When called as a module, draws all those subpaths using `stroke()`.
604// .
605// When called as a module the dash pattern is multiplied by the line width. When called as
606// a function the dash pattern applies as you specify it.
607// Arguments:
608// path = The path or region to subdivide into dashes.
609// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line.
610// ---
611// width = The width of the dashed line to draw. Module only. Default: 1
612// closed = If true, treat path as a closed polygon. Default: false
613// fit = If true, shrink or stretch the dash pattern so that the path ends ofter a logical dash. Default: true
614// roundcaps = (Module only) If true, draws dashes with rounded caps. This often looks better. Default: true
615// mindash = (Function only) Specifies the minimal dash length to return at the end of a path when fit is false. Default: 0.5
616// Example(2D): Open Path
617// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]];
618// dashed_stroke(path, [3,2], width=1);
619// Example(2D): Closed Polygon
620// path = circle(d=100,$fn=72);
621// dashpat = [10,2, 3,2, 3,2];
622// dashed_stroke(path, dashpat, width=1, closed=true);
623// Example(FlatSpin,VPD=250): 3D Dashed Path
624// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]];
625// dashed_stroke(path, [3,2], width=1);
626function dashed_stroke(path, dashpat=[3,3], closed=false, fit=true, mindash=0.5) =
627 is_region(path) ? [
628 for (p = path)
629 each dashed_stroke(p, dashpat, closed=true, fit=fit)
630 ] :
631 let(
632 path = closed? list_wrap(path) : path,
633 dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]),
634 plen = path_length(path),
635 dlen = sum(dashpat),
636 doff = cumsum(dashpat),
637 freps = plen / dlen,
638 reps = max(1, fit? round(freps) : floor(freps)),
639 tlen = !fit? plen :
640 reps * dlen + (closed? 0 : dashpat[0]),
641 sc = plen / tlen,
642 cuts = [
643 for (i = [0:1:reps], off = doff*sc)
644 let (x = i*dlen*sc + off)
645 if (x > 0 && x < plen-EPSILON) x
646 ],
647 dashes = path_cut(path, cuts, closed=false),
648 dcnt = len(dashes),
649 evens = [
650 for (i = idx(dashes))
651 if (i % 2 == 0)
652 let( dash = dashes[i] )
653 if (i < dcnt-1 || path_length(dash) > mindash)
654 dashes[i]
655 ]
656 ) evens;
657
658
659module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, roundcaps=false) {
660 no_children($children);
661 segs = dashed_stroke(path, dashpat=dashpat*width, closed=closed, fit=fit, mindash=0.5*width);
662 for (seg = segs)
663 stroke(seg, width=width, endcaps=roundcaps? "round" : false);
664}
665
666
667
668// Section: Computing paths
669
670// Function&Module: arc()
671// Synopsis: Draws a 2D pie-slice or returns 2D or 3D path forming an arc.
672// SynTags: Geom, Path
673// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
674// See Also: pie_slice(), stroke(), ring()
675//
676// Usage: 2D arc from 0º to `angle` degrees.
677// path=arc(n, r|d=, angle);
678// Usage: 2D arc from START to END degrees.
679// path=arc(n, r|d=, angle=[START,END]);
680// Usage: 2D arc from `start` to `start+angle` degrees.
681// path=arc(n, r|d=, start=, angle=);
682// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis.
683// path=arc(n, width=, thickness=);
684// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`.
685// path=arc(n, cp=, points=[P0,P1], [long=], [cw=], [ccw=]);
686// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`.
687// path=arc(n, points=[P0,P1,P2]);
688// Usage: 2D or 3D arc, fron tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`.
689// path=arc(n, corner=[P0,P1,P2], r=);
690// Usage: Create a wedge using any other arc parameters
691// path=arc(wedge=true,...)
692// Usage: as module
693// arc(...) [ATTACHMENTS];
694// Description:
695// If called as a function, returns a 2D or 3D path forming an arc. If `wedge` is true, the centerpoint of the arc appears as the first point in the result.
696// If called as a module, creates a 2D arc polygon or pie slice shape.
697// Arguments:
698// n = Number of vertices to form the arc curve from.
699// r = Radius of the arc.
700// angle = If a scalar, specifies the end angle in degrees (relative to start parameter). If a vector of two scalars, specifies start and end angles.
701// ---
702// d = Diameter of the arc.
703// cp = Centerpoint of arc.
704// points = Points on the arc.
705// corner = A path of two segments to fit an arc tangent to.
706// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
707// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
708// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
709// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
710// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
711// start = Start angle of arc. Default: 0
712// wedge = If true, include centerpoint `cp` in output to form pie slice shape. Default: false
713// endpoint = If false exclude the last point (function only). Default: true
714// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
715// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
716// Examples(2D):
717// arc(n=4, r=30, angle=30, wedge=true);
718// arc(r=30, angle=30, wedge=true);
719// arc(d=60, angle=30, wedge=true);
720// arc(d=60, angle=120);
721// arc(d=60, angle=120, wedge=true);
722// arc(r=30, angle=[75,135], wedge=true);
723// arc(r=30, start=45, angle=75, wedge=true);
724// arc(width=60, thickness=20);
725// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true);
726// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true);
727// Example(2D): Fit to three points.
728// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
729// Example(2D):
730// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
731// stroke(closed=true, path);
732// Example(FlatSpin,VPD=175):
733// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]);
734// stroke(path, dots=true, dots_color="blue");
735// Example(2D): Fit to a corner.
736// pts = [[0,40], [-40,-10], [30,0]];
737// path = arc(corner=pts, r=20);
738// stroke(pts, endcaps="arrow2");
739// stroke(path, endcap2="arrow2", color="blue");
740function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true) =
741 assert(is_bool(endpoint))
742 !endpoint ?
743 assert(!wedge, "endpoint cannot be false if wedge is true")
744 list_head(arc(u_add(n,1),r,angle,d,cp,points,corner,width,thickness,start,wedge,long,cw,ccw,true))
745 :
746 assert(is_undef(start) || is_def(angle), "start requires angle")
747 assert(is_undef(angle) || !any_defined([thickness,width,points,corner]), "Cannot give angle with points, corner, width or thickness")
748 assert(is_undef(n) || (is_integer(n) && n>=2), "Number of points must be an integer 2 or larger")
749 assert(is_undef(points) || is_path(points, [2,3]), "Points must be a list of 2d or 3d points")
750 assert((is_def(points) && len(points)==2) || !any([cw,ccw,long]), "cw, ccw, and long are only allowed when points is a list of length 2")
751 // First try for 2D arc specified by width and thickness
752 is_def(width) && is_def(thickness)?
753 assert(!any_defined([r,cp,points,angle,start]),"Conflicting or invalid parameters to arc")
754 assert(width>0, "Width must be postive")
755 assert(thickness>0, "Thickness must be positive")
756 arc(n,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge)
757 : is_def(angle)?
758 let(
759 parmok = !any_defined([points,width,thickness]) &&
760 ((is_vector(angle,2) && is_undef(start)) || is_finite(angle))
761 )
762 assert(parmok,"Invalid parameters in arc")
763 let(
764 cp = first_defined([cp,[0,0]]),
765 start = is_def(start)? start : is_vector(angle) ? angle[0] : 0,
766 angle = is_vector(angle)? angle[1]-angle[0] : angle,
767 r = get_radius(r=r, d=d)
768 )
769 assert(is_vector(cp,2),"Centerpoint must be a 2d vector")
770 assert(angle!=0, "Arc has zero length")
771 assert(is_def(r) && r>0, "Arc radius invalid")
772 let(
773 n = is_def(n) ? n : max(3, ceil(segs(r)*abs(angle)/360)),
774 arcpoints = [for(i=[0:n-1]) let(theta = start + i*angle/(n-1)) r*[cos(theta),sin(theta)]+cp]
775 )
776 [
777 if (wedge) cp,
778 each arcpoints
779 ]
780 : is_def(corner)?
781 assert(is_path(corner,[2,3]) && len(corner)==3,str("Point list is invalid"))
782 assert(is_undef(cp) && !any([long,cw,ccw]), "Cannot use cp, long, cw, or ccw with corner")
783 // Arc is 3D, so transform corner to 2D and make a recursive call, then remap back to 3D
784 len(corner[0]) == 3? (
785 let(
786 plane = [corner[2], corner[0], corner[1]],
787 points2d = project_plane(plane, corner)
788 )
789 lift_plane(plane,arc(n,corner=points2d,wedge=wedge,long=long))
790 ) :
791 assert(is_path(corner) && len(corner) == 3)
792 let(col = is_collinear(corner[0],corner[1],corner[2]))
793 assert(!col, "Collinear inputs do not define an arc")
794 let( r = get_radius(r=r, d=d) )
795 assert(is_finite(r) && r>0, "Must specify r= or d= when corner= is given.")
796 let(
797 ci = circle_2tangents(r, corner[0], corner[1], corner[2], tangents=true),
798 cp = ci[0], nrm = ci[1], tp1 = ci[2], tp2 = ci[3],
799 dir = det2([corner[1]-corner[0],corner[2]-corner[1]]) > 0,
800 corner = dir? [tp1,tp2] : [tp2,tp1],
801 theta_start = atan2(corner[0].y-cp.y, corner[0].x-cp.x),
802 theta_end = atan2(corner[1].y-cp.y, corner[1].x-cp.x),
803 angle = posmod(theta_end-theta_start, 360),
804 arcpts = arc(n,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge)
805 )
806 dir ? arcpts : wedge ? reverse_polygon(arcpts) : reverse(arcpts)
807 : assert(is_def(points), "Arc not specified: must give points, angle, or width and thickness")
808 assert(is_path(points,[2,3]),"Point list is invalid")
809 // If arc is 3D, transform points to 2D and make a recursive call, then remap back to 3D
810 len(points[0]) == 3?
811 assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false")
812 assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d")
813 let(
814 plane = [is_def(cp) ? cp : points[2], points[0], points[1]],
815 center2d = is_def(cp) ? project_plane(plane,cp) : undef,
816 points2d = project_plane(plane, points)
817 )
818 lift_plane(plane,arc(n,cp=center2d,points=points2d,wedge=wedge,long=long))
819 : len(points)==2?
820 // Arc defined by center plus two points, will have radius defined by center and points[0]
821 // and extent defined by direction of point[1] from the center
822 assert(is_vector(cp,2), "Centerpoint is required when points has length 2 and it must be a 2d vector")
823 assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed")
824 assert(points[0]!=points[1], "Arc endpoints are equal")
825 assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint")
826 assert(num_true([long,cw,ccw])<=1, str("Only one of `long`, `cw` and `ccw` can be true",cw,ccw,long))
827 let(
828 angle = vector_angle(points[0], cp, points[1]),
829 v1 = points[0]-cp,
830 v2 = points[1]-cp,
831 prelim_dir = sign(det2([v1,v2])), // z component of cross product
832 dir = prelim_dir != 0 ? prelim_dir :
833 assert(cw || ccw, "Collinear inputs don't define a unique arc")
834 1,
835 r = norm(v1),
836 final_angle = long || (ccw && dir<0) || (cw && dir>0) ?
837 -dir*(360-angle) :
838 dir*angle,
839 sa = atan2(v1.y,v1.x)
840 )
841 arc(n,cp=cp,r=r,start=sa,angle=final_angle,wedge=wedge)
842 : // Final case is arc passing through three points, starting at point[0] and ending at point[3]
843 let(col = is_collinear(points[0],points[1],points[2]))
844 assert(!col, "Collinear inputs do not define an arc")
845 let(
846 cp = line_intersection(_normal_segment(points[0],points[1]),_normal_segment(points[1],points[2])),
847 // select order to be counterclockwise
848 dir = det2([points[1]-points[0],points[2]-points[1]]) > 0,
849 points = dir? select(points,[0,2]) : select(points,[2,0]),
850 r = norm(points[0]-cp),
851 theta_start = atan2(points[0].y-cp.y, points[0].x-cp.x),
852 theta_end = atan2(points[1].y-cp.y, points[1].x-cp.x),
853 angle = posmod(theta_end-theta_start, 360),
854 // Specify endpoints exactly; skip those endpoints when producing arc points
855 // Generating the whole arc and clipping ends is the easiest way to ensure that we
856 // generate the proper number of points.
857 arcpts = [ if (wedge) cp,
858 points[0],
859 each select(arc(n,cp=cp,r=r,start=theta_start,angle=angle),1,-2),
860 points[1]
861 ]
862
863 )
864 dir ? arcpts
865 : wedge ? reverse_polygon(arcpts) // Keep the centerpoint at position 0 in the list
866 : reverse(arcpts);
867
868
869module arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, anchor=CENTER, spin=0)
870{
871 path = arc(n=n, r=r, angle=angle, d=d, cp=cp, points=points, corner=corner, width=width, thickness=thickness, start=start, wedge=wedge);
872 attachable(anchor,spin, two_d=true, path=path, extent=false) {
873 polygon(path);
874 children();
875 }
876}
877
878
879// Function: catenary()
880// Synopsis: Returns a 2D Catenary chain or arch path.
881// SynTags: Path
882// Topics: Paths
883// See Also: circle(), stroke()
884// Usage:
885// path = catenary(width, droop=|angle=, n=);
886// Description:
887// Returns a 2D Catenary path, which is the path a chain held at both ends will take.
888// The path will have the endpoints at `[±width/2, 0]`, and the middle of the path will droop
889// towards Y- if the given droop= or angle= is positive. It will droop towards Y+ if the
890// droop= or angle= is negative. You *must* specify one of droop= or angle=.
891// Arguments:
892// width = The straight-line distance between the endpoints of the path.
893// droop = If given, specifies the height difference between the endpoints and the hanging middle of the path. If given a negative value, returns an arch *above* the Y axis.
894// n = The number of points to return in the path. Default: 100
895// ---
896// angle = If given, specifies the angle that the path will droop by at the endpoints. If given a negative value, returns an arch *above* the Y axis.
897// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
898// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
899// Example(2D): By Droop
900// stroke(catenary(100, droop=30));
901// Example(2D): By Angle
902// stroke(catenary(100, angle=30));
903// Example(2D): Upwards Arch by Angle
904// stroke(catenary(100, angle=30));
905// Example(2D): Upwards Arch by Height Delta
906// stroke(catenary(100, droop=-30));
907// Example(2D): Specifying Vertex Count
908// stroke(catenary(100, angle=-85, n=11), dots="dot");
909// Example(3D): Sweeping a Catenary Path
910// path = xrot(90, p=path3d(catenary(100, droop=20, n=41)));
911// path_sweep(circle(r=1.5, $fn=24), path);
912function catenary(width, droop, n=100, angle) =
913 assert(one_defined([droop, angle],"droop,angle"))
914 let(
915 sgn = is_undef(droop)? sign(angle) : sign(droop),
916 droop = droop==undef? undef : abs(droop),
917 angle = angle==undef? undef : abs(angle)
918 )
919 assert(is_finite(width) && width>0, "Bad width= value.")
920 assert(is_integer(n) && n>0, "Bad n= value. Must be a positive integer.")
921 assert(is_undef(droop) || is_finite(droop), "Bad droop= value.")
922 assert(is_undef(angle) || (is_finite(angle) && angle != 0 && abs(angle) < 90), "Bad angle= value.")
923 let(
924 catlup_fn = is_undef(droop)
925 ? function(x) let(
926 p1 = [x-0.001, cosh(x-0.001)-1],
927 p2 = [x+0.001, cosh(x+0.001)-1],
928 delta = p2-p1,
929 ang = atan2(delta.y, delta.x)
930 ) ang
931 : function(x) (cosh(x)-1)/x,
932 binsearch_fn = function(targ,x=0,inc=4)
933 inc < 1e-9? lookup(targ,[[catlup_fn(x),x],[catlup_fn(x+inc),x+inc]]) :
934 catlup_fn(x+inc) > targ? binsearch_fn(targ,x,inc/2) :
935 binsearch_fn(targ,x+inc,inc),
936 scx = is_undef(droop)? binsearch_fn(angle) :
937 binsearch_fn(droop / (width/2)),
938 sc = width/2 / scx,
939 droop = !is_undef(droop)? droop : (cosh(scx)-1) * sc,
940 path = [
941 for (x = lerpn(-scx,scx,n))
942 let(
943 xval = x * sc,
944 yval = approx(abs(x),scx)? 0 :
945 (cosh(x)-1) * sc - droop
946 )
947 [xval, yval]
948 ],
949 out = sgn>0? path : yflip(p=path)
950 ) out;
951
952
953module catenary(width, droop, n=100, angle, anchor=CTR, spin=0) {
954 path = catenary(width=width, droop=droop, n=n, angle=angle);
955 attachable(anchor,spin, two_d=true, path=path, extent=true) {
956 polygon(path);
957 children();
958 }
959}
960
961
962// Function: helix()
963// Synopsis: Creates a 2d spiral or 3d helical path.
964// SynTags: Path
965// Topics: Path Generators, Paths, Drawing Tools
966// See Also: pie_slice(), stroke(), thread_helix(), path_sweep()
967//
968// Usage:
969// path = helix(l|h, [turns=], [angle=], r=|r1=|r2=, d=|d1=|d2=);
970// Description:
971// Returns a 3D helical path on a cone, including the degerate case of flat spirals.
972// You can specify start and end radii. You can give the length, the helix angle, or the number of turns: two
973// of these three parameters define the helix. For a flat helix you must give length 0 and a turn count.
974// Helix will be right handed if turns is positive and left handed if it is negative.
975// The angle is calculateld based on the radius at the base of the helix.
976// Arguments:
977// h/l = Height/length of helix, zero for a flat spiral
978// ---
979// turns = Number of turns in helix, positive for right handed
980// angle = helix angle
981// r = Radius of helix
982// r1 = Radius of bottom of helix
983// r2 = Radius of top of helix
984// d = Diameter of helix
985// d1 = Diameter of bottom of helix
986// d2 = Diameter of top of helix
987// Example(3D):
988// stroke(helix(turns=2.5, h=100, r=50), dots=true, dots_color="blue");
989// Example(3D): Helix that turns the other way
990// stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue");
991// Example(3D): Flat helix (note points are still 3d)
992// stroke(helix(h=0,r1=50,r2=25,l=0, turns=4));
993module helix(l,h,turns,angle, r, r1, r2, d, d1, d2) {no_module();}
994function helix(l,h,turns,angle, r, r1, r2, d, d1, d2)=
995 let(
996 r1=get_radius(r=r,r1=r1,d=d,d1=d1,dflt=1),
997 r2=get_radius(r=r,r1=r2,d=d,d1=d2,dflt=1),
998 length = first_defined([l,h])
999 )
1000 assert(num_defined([length,turns,angle])==2,"Must define exactly two of l/h, turns, and angle")
1001 assert(is_undef(angle) || length!=0, "Cannot give length 0 with an angle")
1002 let(
1003 // length advances dz for each turn
1004 dz = is_def(angle) && length!=0 ? 2*PI*r1*tan(angle) : length/abs(turns),
1005
1006 maxtheta = is_def(turns) ? 360*turns : 360*length/dz,
1007 N = segs(max(r1,r2))
1008 )
1009 [for(theta=lerpn(0,maxtheta, max(3,ceil(abs(maxtheta)*N/360))))
1010 let(R=lerp(r1,r2,theta/maxtheta))
1011 [R*cos(theta), R*sin(theta), abs(theta)/360 * dz]];
1012
1013
1014function _normal_segment(p1,p2) =
1015 let(center = (p1+p2)/2)
1016 [center, center + norm(p1-p2)/2 * line_normal(p1,p2)];
1017
1018
1019// Function: turtle()
1020// Synopsis: Uses [turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) to generate a 2D path.
1021// SynTags: Path
1022// Topics: Shapes (2D), Path Generators (2D), Mini-Language
1023// See Also: turtle3d(), stroke(), path_sweep()
1024// Usage:
1025// path = turtle(commands, [state], [full_state=], [repeat=])
1026// Description:
1027// Use a sequence of [turtle graphics]{https://en.wikipedia.org/wiki/Turtle_graphics} commands to generate a path. The parameter `commands` is a list of
1028// turtle commands and optional parameters for each command. The turtle state has a position, movement direction,
1029// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the
1030// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just
1031// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state.
1032// You can invoke `turtle` again with this full state to continue the turtle path where you left off.
1033// .
1034// The turtle state is a list with three entries: the path constructed so far, the current step as a 2-vector, the current default angle,
1035// and the current arcsteps setting.
1036// .
1037// Commands | Arguments | What it does
1038// ------------ | ------------------ | -------------------------------
1039// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1.
1040// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction.
1041// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction.
1042// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction.
1043// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable.
1044// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if ytarget is not reachable.
1045// "jump" | point | Move the turtle to the specified point
1046// "xjump" | x | Move the turtle's x position to the specified value
1047// "yjump | y | Move the turtle's y position to the specified value
1048// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90.
1049// "left" | [angle] | Same as "turn"
1050// "right" | [angle] | Same as "turn", -angle
1051// "angle" | angle | Set the default turn angle.
1052// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector.
1053// "length" | length | Change the turtle move distance to `length`
1054// "scale" | factor | Multiply turtle move distance by `factor`
1055// "addlength" | length | Add `length` to the turtle move distance
1056// "repeat" | count, commands | Repeats a list of commands `count` times.
1057// "arcleft" | radius, [angle] | Draw an arc from the current position toward the left at the specified radius and angle. The turtle turns by `angle`. A negative angle draws the arc to the right instead of the left, and leaves the turtle facing right. A negative radius draws the arc to the right but leaves the turtle facing left.
1058// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle
1059// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle.
1060// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle.
1061// "arcsteps" | count | Specifies the number of segments to use for drawing arcs. If you set it to zero then the standard `$fn`, `$fa` and `$fs` variables define the number of segments.
1062//
1063// Arguments:
1064// commands = List of turtle commands
1065// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right.
1066// ---
1067// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false
1068// repeat = Number of times to repeat the command list. Default: 1
1069//
1070// Example(2D): Simple rectangle
1071// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]);
1072// stroke(path,width=.1);
1073// Example(2D): Pentagon
1074// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]);
1075// stroke(path,width=.1,closed=true);
1076// Example(2D): Pentagon using the repeat argument
1077// path=turtle(["move","turn",360/5],repeat=5);
1078// stroke(path,width=.1,closed=true);
1079// Example(2D): Pentagon using the repeat turtle command, setting the turn angle
1080// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]);
1081// stroke(path,width=.1,closed=true);
1082// Example(2D): Pentagram
1083// path = turtle(["move","left",144], repeat=4);
1084// stroke(path,width=.05,closed=true);
1085// Example(2D): Sawtooth path
1086// path = turtle([
1087// "turn", 55,
1088// "untily", 2,
1089// "turn", -55-90,
1090// "untily", 0,
1091// "turn", 55+90,
1092// "untily", 2.5,
1093// "turn", -55-90,
1094// "untily", 0,
1095// "turn", 55+90,
1096// "untily", 3,
1097// "turn", -55-90,
1098// "untily", 0
1099// ]);
1100// stroke(path, width=.1);
1101// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump".
1102// path = turtle([
1103// "turn", 55,
1104// "untily", 2,
1105// "yjump", 0,
1106// "untily", 2.5,
1107// "yjump", 0,
1108// "untily", 3,
1109// "yjump", 0,
1110// ]);
1111// stroke(path, width=.1);
1112// Example(2DMed): square spiral
1113// path = turtle(["move","left","addlength",1],repeat=50);
1114// stroke(path,width=.2);
1115// Example(2DMed): pentagonal spiral
1116// path = turtle(["move","left",360/5,"addlength",1],repeat=50);
1117// stroke(path,width=.7);
1118// Example(2DMed): yet another spiral, without using `repeat`
1119// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50))));
1120// stroke(path,width=.7);
1121// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not.
1122// path = turtle(["move","left",71,"scale",1.05],repeat=50);
1123// stroke(path,width=.15);
1124// Example(2D): Koch Snowflake
1125// function koch_unit(depth) =
1126// depth==0 ? ["move"] :
1127// concat(
1128// koch_unit(depth-1),
1129// ["right"],
1130// koch_unit(depth-1),
1131// ["left","left"],
1132// koch_unit(depth-1),
1133// ["right"],
1134// koch_unit(depth-1)
1135// );
1136// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]);
1137// polygon(turtle(koch));
1138module turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) {no_module();}
1139function turtle(commands, state=[[[0,0]],[1,0],90,0], full_state=false, repeat=1) =
1140 let( state = is_vector(state) ? [[state],[1,0],90,0] : state )
1141 repeat == 1?
1142 _turtle(commands,state,full_state) :
1143 _turtle_repeat(commands, state, full_state, repeat);
1144
1145function _turtle_repeat(commands, state, full_state, repeat) =
1146 repeat==1?
1147 _turtle(commands,state,full_state) :
1148 _turtle_repeat(commands, _turtle(commands, state, true), full_state, repeat-1);
1149
1150function _turtle_command_len(commands, index) =
1151 let( one_or_two_arg = ["arcleft","arcright", "arcleftto", "arcrightto"] )
1152 commands[index] == "repeat"? 3 : // Repeat command requires 2 args
1153 // For these, the first arg is required, second arg is present if it is not a string
1154 in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) ? 3 :
1155 is_string(commands[index+1])? 1 : // If 2nd item is a string it's must be a new command
1156 2; // Otherwise we have command and arg
1157
1158function _turtle(commands, state, full_state, index=0) =
1159 index < len(commands) ?
1160 _turtle(commands,
1161 _turtle_command(commands[index],commands[index+1],commands[index+2],state,index),
1162 full_state,
1163 index+_turtle_command_len(commands,index)
1164 ) :
1165 ( full_state ? state : state[0] );
1166
1167// Turtle state: state = [path, step_vector, default angle, default arcsteps]
1168
1169function _turtle_command(command, parm, parm2, state, index) =
1170 command == "repeat"?
1171 assert(is_num(parm),str("\"repeat\" command requires a numeric repeat count at index ",index))
1172 assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index))
1173 _turtle_repeat(parm2, state, true, parm) :
1174 let(
1175 path = 0,
1176 step=1,
1177 angle=2,
1178 arcsteps=3,
1179 parm = !is_string(parm) ? parm : undef,
1180 parm2 = !is_string(parm2) ? parm2 : undef,
1181 needvec = ["jump", "xymove"],
1182 neednum = ["untilx","untily","xjump","yjump","angle","length","scale","addlength"],
1183 needeither = ["setdir"],
1184 chvec = !in_list(command,needvec) || is_vector(parm,2),
1185 chnum = !in_list(command,neednum) || is_num(parm),
1186 vec_or_num = !in_list(command,needeither) || (is_num(parm) || is_vector(parm,2)),
1187 lastpt = last(state[path])
1188 )
1189 assert(chvec,str("\"",command,"\" requires a vector parameter at index ",index))
1190 assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index))
1191 assert(vec_or_num,str("\"",command,"\" requires a vector or numeric parameter at index ",index))
1192
1193 command=="move" ? list_set(state, path, concat(state[path],[default(parm,1)*state[step]+lastpt])) :
1194 command=="untilx" ? (
1195 let(
1196 int = line_intersection([lastpt,lastpt+state[step]], [[parm,0],[parm,1]]),
1197 xgood = sign(state[step].x) == sign(int.x-lastpt.x)
1198 )
1199 assert(xgood,str("\"untilx\" never reaches desired goal at index ",index))
1200 list_set(state,path,concat(state[path],[int]))
1201 ) :
1202 command=="untily" ? (
1203 let(
1204 int = line_intersection([lastpt,lastpt+state[step]], [[0,parm],[1,parm]]),
1205 ygood = is_def(int) && sign(state[step].y) == sign(int.y-lastpt.y)
1206 )
1207 assert(ygood,str("\"untily\" never reaches desired goal at index ",index))
1208 list_set(state,path,concat(state[path],[int]))
1209 ) :
1210 command=="xmove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[1,0]+lastpt])):
1211 command=="ymove" ? list_set(state, path, concat(state[path],[default(parm,1)*norm(state[step])*[0,1]+lastpt])):
1212 command=="xymove" ? list_set(state, path, concat(state[path], [lastpt+parm])):
1213 command=="jump" ? list_set(state, path, concat(state[path],[parm])):
1214 command=="xjump" ? list_set(state, path, concat(state[path],[[parm,lastpt.y]])):
1215 command=="yjump" ? list_set(state, path, concat(state[path],[[lastpt.x,parm]])):
1216 command=="turn" || command=="left" ? list_set(state, step, rot(default(parm,state[angle]),p=state[step])) :
1217 command=="right" ? list_set(state, step, rot(-default(parm,state[angle]),p=state[step])) :
1218 command=="angle" ? list_set(state, angle, parm) :
1219 command=="setdir" ? (
1220 is_vector(parm) ?
1221 list_set(state, step, norm(state[step]) * unit(parm)) :
1222 list_set(state, step, norm(state[step]) * [cos(parm),sin(parm)])
1223 ) :
1224 command=="length" ? list_set(state, step, parm*unit(state[step])) :
1225 command=="scale" ? list_set(state, step, parm*state[step]) :
1226 command=="addlength" ? list_set(state, step, state[step]+unit(state[step])*parm) :
1227 command=="arcsteps" ? list_set(state, arcsteps, parm) :
1228 command=="arcleft" || command=="arcright" ?
1229 assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))
1230 let(
1231 myangle = default(parm2,state[angle]),
1232 lrsign = command=="arcleft" ? 1 : -1,
1233 radius = parm*sign(myangle),
1234 center = lastpt + lrsign*radius*line_normal([0,0],state[step]),
1235 steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
1236 arcpath = myangle == 0 || radius == 0 ? [] : arc(
1237 steps,
1238 points = [
1239 lastpt,
1240 rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle/2),
1241 rot(cp=center, p=lastpt, a=sign(parm)*lrsign*myangle)
1242 ]
1243 )
1244 )
1245 list_set(
1246 state, [path,step], [
1247 concat(state[path], list_tail(arcpath)),
1248 rot(lrsign * myangle,p=state[step])
1249 ]
1250 ) :
1251 command=="arcleftto" || command=="arcrightto" ?
1252 assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))
1253 assert(is_num(parm2),str("\"",command,"\" command requires a numeric angle value at index ",index))
1254 let(
1255 radius = parm,
1256 lrsign = command=="arcleftto" ? 1 : -1,
1257 center = lastpt + lrsign*radius*line_normal([0,0],state[step]),
1258 steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
1259 start_angle = posmod(atan2(state[step].y, state[step].x),360),
1260 end_angle = posmod(parm2,360),
1261 delta_angle = -start_angle + (lrsign * end_angle < lrsign*start_angle ? end_angle+lrsign*360 : end_angle),
1262 arcpath = delta_angle == 0 || radius==0 ? [] : arc(
1263 steps,
1264 points = [
1265 lastpt,
1266 rot(cp=center, p=lastpt, a=sign(radius)*delta_angle/2),
1267 rot(cp=center, p=lastpt, a=sign(radius)*delta_angle)
1268 ]
1269 )
1270 )
1271 list_set(
1272 state, [path,step], [
1273 concat(state[path], list_tail(arcpath)),
1274 rot(delta_angle,p=state[step])
1275 ]
1276 ) :
1277 assert(false,str("Unknown turtle command \"",command,"\" at index",index))
1278 [];
1279
1280
1281// Section: Debugging polygons
1282
1283// Module: debug_polygon()
1284// Synopsis: Draws an annotated polygon.
1285// SynTags: Geom
1286// Topics: Shapes (2D)
1287// See Also: debug_region(), debug_vnf(), debug_bezier()
1288//
1289// Usage:
1290// debug_polygon(points, paths, [vertices=], [edges=], [convexity=], [size=]);
1291// Description:
1292// A drop-in replacement for `polygon()` that renders and labels the path points and
1293// edges. The start of each path is marked with a blue circle and the end with a pink diamond.
1294// You can suppress the display of vertex or edge labeling using the `vertices` and `edges` arguments.
1295// Arguments:
1296// points = The array of 2D polygon vertices.
1297// paths = The path connections between the vertices.
1298// ---
1299// vertices = if true display vertex labels and start/end markers. Default: true
1300// edges = if true display edge labels. Default: true
1301// convexity = The max number of walls a ray can pass through the given polygon paths.
1302// size = The base size of the line and labels.
1303// Example(Big2D):
1304// debug_polygon(
1305// points=concat(
1306// regular_ngon(or=10, n=8),
1307// regular_ngon(or=8, n=8)
1308// ),
1309// paths=[
1310// [for (i=[0:7]) i],
1311// [for (i=[15:-1:8]) i]
1312// ]
1313// );
1314module debug_polygon(points, paths, vertices=true, edges=true, convexity=2, size=1)
1315{
1316 no_children($children);
1317 print_paths=is_def(paths);
1318 echo(points=points);
1319 if (print_paths)
1320 echo(paths=paths);
1321 paths = is_undef(paths)? [count(points)] :
1322 is_num(paths[0])? [paths] :
1323 paths;
1324 linear_extrude(height=0.01, convexity=convexity, center=true) {
1325 polygon(points=points, paths=paths, convexity=convexity);
1326 }
1327 if (vertices)
1328 _debug_poly_verts(points,size);
1329 if (edges)
1330 for (j = [0:1:len(paths)-1]) _debug_poly_edges(j, points, paths[j], vertices, size);
1331}
1332
1333
1334module _debug_poly_verts(points, size)
1335{
1336 labels=is_vector(points[0]) ? [for(i=idx(points)) str(i)]
1337 :[for(j=idx(points), i=idx(points[j])) str(chr(97+j),i)];
1338 points = is_vector(points[0]) ? points : flatten(points);
1339 dups = vector_search(points, EPSILON, points);
1340 color("red") {
1341 for (ind=dups){
1342 numstr = str_join(select(labels,ind),",");
1343 up(0.2) {
1344 translate(points[ind[0]]) {
1345 linear_extrude(height=0.1, convexity=10, center=true) {
1346 text(text=numstr, size=size, halign="center", valign="center");
1347 }
1348 }
1349 }
1350 }
1351 }
1352}
1353
1354
1355module _debug_poly_edges(j,points, path,vertices,size)
1356{
1357 path = default(path, count(len(points)));
1358 if (vertices){
1359 translate(points[path[0]]) {
1360 color("cyan") up(0.1) cylinder(d=size*1.5, h=0.01, center=false, $fn=12);
1361 }
1362 translate(points[path[len(path)-1]]) {
1363 color("pink") up(0.11) cylinder(d=size*1.5, h=0.01, center=false, $fn=4);
1364 }
1365 }
1366 for (i = [0:1:len(path)-1]) {
1367 midpt = (points[path[i]] + points[path[(i+1)%len(path)]])/2;
1368 color("blue") {
1369 up(0.2) {
1370 translate(midpt) {
1371 linear_extrude(height=0.1, convexity=10, center=true) {
1372 text(text=str(chr(65+j),i), size=size/2, halign="center", valign="center");
1373 }
1374 }
1375 }
1376 }
1377 }
1378 }
1379
1380// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap